//
//  Please Note: This is a collection of small static helper functions and is not intended to be imported and used directly.
//  Subtle changes to a static helper function may unexpectedly cause older legacy code to break.
//  As such, it's recommended that you copy needed functions into your code and use them there rather than import them from here.
//


package com.lowke
{
    import flash.display.DisplayObject;
    import flash.display.DisplayObjectContainer;
    import flash.display.FrameLabel;
    import flash.display.Graphics;
    import flash.display.LoaderInfo;
    import flash.display.MovieClip;
    import flash.display.Shape;
    import flash.display.Sprite;
    import flash.display.Stage;
    import flash.events.Event;
    import flash.events.IEventDispatcher;
    import flash.geom.ColorTransform;
    import flash.geom.Point;
    import flash.geom.Rectangle;
    import flash.geom.Transform;
    import flash.media.Sound;
    import flash.media.SoundTransform;
    import flash.system.System;
    import flash.text.Font;
    import flash.utils.ByteArray;
    import flash.utils.describeType;
    import flash.utils.getDefinitionByName;
    import flash.utils.getQualifiedClassName;
    
    /**
     * @author Russell Lowke
     */
    public class Util
    {
        /**
         * Generates a random integer between low and high.
         * 
         * @param low Lowest possible integer generated.
         * @param high Highest possible integer generated.
         * @return Returns a random integer between low and high.
         */
        public static function randomInt(low:int, high:int):int 
        {
            var range:int = (high + 1) - low;
            var r:int = Math.floor(Math.random() * range) + low;
            
            return r;
        }
        
        
        // returned by getFrameNumber(), indicates no frame found with that label
        public static const NO_FRAME:int = 0;   
        
        /**
         * Returns the frame number of a specifc label on a MovieClip.
         * 
         * @param clip The MovieClip with labels.
         * @param label The label being searched for.
         * @return Returns the frame number of the label, -1 returned if no label found.
         */
        public static function getFrameNumber(clip:MovieClip, 
                                              label:String):uint 
        {
            if (! clip) 
            {
                return NO_FRAME;
            }
            
            var frameLabels:Array = clip.currentLabels;
            for (var i:uint = 0; i < frameLabels.length; ++i) 
            {
                var frameLabel:FrameLabel = frameLabels[i];
                if (frameLabel.name == label) 
                {
                    return frameLabel.frame;
                }
            }
            return NO_FRAME;
        }
        
        
        /**
         * Returns the next FrameLabel on a MovieClip after the current frame.
         * If there is no next label then the last label is given.
         * 
         * @param clip The MovieClip with labels.
         */
        public static function getNextLabel(clip:MovieClip):FrameLabel
        {
            var frameLabels:Array = clip.currentLabels;
            if (! frameLabels.length) 
            {
                return null;    // no frame labels
            }
            
            for (var i:uint = 0; i < frameLabels.length; ++i) 
            {
                var frameLabel:FrameLabel = frameLabels[i];
                if (frameLabel.frame > clip.currentFrame) 
                {
                    return frameLabel;
                }
            }
            
            // otherwise return the last label
            return frameLabels[frameLabels.length - 1];
        }
        
        
        /**
         * Goto the next label in a MovieClip
         * 
         * @param clip The MovieClip with labels
         */
        public static function gotoNextLabel(clip:MovieClip):void
        {
            var frameLabel:FrameLabel = getNextLabel(clip);
            if (frameLabel) 
            {
                clip.gotoAndStop(frameLabel.frame);
            }
        }
        
        
        /**
         * Returns the previous FrameLabel on a MovieClip before the current frame.
         * If there is no previous label then the first label is given.
         * 
         * @param clip The MovieClip with labels.
         */
        public static function getPreviousLabel(clip:MovieClip):FrameLabel
        {
            var frameLabels:Array = clip.currentLabels;
            if (! frameLabels.length) 
            {
                return null;    // no frame labels
            }
            
            var index:int;
            for (var i:uint = 0; i < frameLabels.length; ++i) 
            {
                var frameLabel:FrameLabel = frameLabels[i];
                if (frameLabel.frame > clip.currentFrame) 
                {
                    return frameLabels[(i < 2) ? 0 : i - 2];
                }
            }
            
            // must be at or beyond the last label so  
            //  return the second last label
            return frameLabels[(frameLabels.length > 2) ? frameLabels.length - 2 : 0];
        }
        
        
        /**
         * Goto the previous label in a MovieClip
         * 
         * @param clip The MovieClip with labels
         */
        public static function gotoPreviousLabel(clip:MovieClip):void
        {
            var frameLabel:FrameLabel = getPreviousLabel(clip);
            if (frameLabel) 
            {
                clip.gotoAndStop(frameLabel.frame);
            }
        }
        
        
        /**
         * Returns a string with a dump of pertinant contents of any 
         * of the variables specified in values Array. 
         * Variables with null, 0, NaN, false and "" values are skipped.
         * 
         * @param data Object being inspected.
         * @param values Array of values to be dumped
         * @return Dump of the pertinant contents of the Object as String.
         */
        public static function dumpValues(object:Object, values:Array):String 
        {
            var str:String = "[ ";
            for each(var key:String in values) 
            {
                var value:Object = object[key];
                if (value != null &&
                    ((value is int || value is uint || value is Number) && value != 0 && ! isNaN(value as Number)) ||
                    (value is Boolean && value == true) ||
                    (value is String && value != "")) {
                    
                    if (str != "[ ") 
                    {
                        str += ", ";
                    }
                    str += key + ":"; 
                    
                    if (value is String) 
                    {
                        str += "\"" + value + "\"";
                    } 
                    else 
                    {
                        str += value;
                    }
                }
            }
            str += " ]";
            
            return str;
        }
        
        
        /**
         * Searches for a registered font by font name and returns that font
         * 
         * @param fontName Name of the font being searched for
         * @param includeDeviceFonts If true then device fonts are included in the search
         * 
         * @returns Returns a reference to the registered font
         *          If the font is not registered <code>null</code> is returned.
         */
        public static function getFont(fontName:String, 
                                       includeDeviceFonts:Boolean = false):Font
        {
            for each (var font:Font in Font.enumerateFonts(includeDeviceFonts)) 
            {
                if (fontName == font.fontName) 
                {
                    return font;    
                }
            }
            return null;
        }
        
        
        /**
         * Gives a dump of all registered fonts
         */
        public static function dumpRegisteredFonts():void
        {
            for each (var font:Font in Font.enumerateFonts(false)) 
            {
                trace("Registered font:" + font.fontName);
            }
            trace("___");
        }
        
        
        /**
         * Strings read from xml files don't have the escape sequences such 
         * as new line recognized in the string. This utility puts the New Line, 
         * Carriage Return and Tab escape sequences back in.
         * 
         * @param string String being scanned for escape sequences.
         * @return The fixed string is returned.
         */
        public static function fixEscapeSequences(string:String):String
        {
            var fixedString:String = string;
            var array:Array = string.split('\\');
            if (array.length > 0) 
            {
                fixedString = array[0];
                for (var i:uint = 1; i < array.length; ++i) 
                {
                    var line:String = array[i];
                    var char:String = line.substr(0, 1);
                    line = line.substr(1);
                    switch (char) 
                    {
                        case 'n':                   // New Line
                            line = '\n' + line;
                            break;
                        case 'r':                   // Carriage Return
                            line = '\r' + line;
                            break;
                        case 't':                   // Tab
                            line = '\t' + line;
                            break;
                    }
                    fixedString += line;
                }
            }
            
            return fixedString;
        }
        
        
        /**
         * A way to force garbage collection in the flash debugger player is to call System.gc() twice.
         * The first call marks items for collection, the second sweeps marked items.
         */
        public static function garbageCollect():void 
        {
            System.gc();
            System.gc();
        }
        
        
        /**
         * With strings coming from XML it's often handy to strip whitespace.
         */
        public static function stripWhitespace(str:String):String 
        {
            var whitespace:RegExp = /(\t|\n|\s{2,})/g;
            str = str.replace(whitespace, "");
            return str;
        }
        
        
        /**
         * Compares two arrays and returns a boolean indicating whether the arrays
         * contain the same values at the same indexes.
         * 
         * @param arr1 The first array that will be compared to the second.
         * @param arr2 The second array that will be compared to the first.
         * @return True if the arrays contains the same values at the same indexes.
         *          False if they do not.
         */     
        public static function arraysAreEqual(arr1:Array, arr2:Array):Boolean
        {
            if (arr1.length != arr2.length) 
            {
                return false;
            }
            
            var len:Number = arr1.length;
            
            for (var i:Number = 0; i < len; i++) 
            {
                if (arr1[i] !== arr2[i]) 
                {
                    return false;
                }
            }
            
            return true;
        }
        
        
        /**
         * Insterts a value into an array at position
         * 
         * @param array Array being modified
         * @param value Value being inserted
         * @param index Position value is inserted into array
         * @return Reference to the modified array
         */
        public static function insertIntoArray(array:Array, value:*, index:uint):Array 
        {
            return array.splice(index, 0, value);
        }
        
        
        /**
         * Removes a value from an array
         * 
         * @param array Array being modified
         * @param index Position of value being removed from array
         * @return Reference to the modified array
         */
        public static function removeIndex(arr:Array, index:uint):Array 
        {
            return arr.splice(index, 1);
        }
        
        
        /**
         * Copies an array of references
         */
        public static function copyReferences(array:Array):Array 
        {
            return array.map(function (e:*):* { return e; });
        }
        
		
		/**
		 * Bring a sprite to the top of its display list.
		 * 
		 * @param sprite Sprite to be put on the top of its display list.
		 */
		public static function bringToTop(sprite:Sprite):void 
		{
			sprite.parent.setChildIndex(sprite, sprite.parent.numChildren - 1);
		}
        
		
		/**
		 * Return true if mouse over sprite.
		 * 
		 * @param sprite Sprite mouse over is being checked for.
		 * @param shapeFlag If true sprite region is checked for (slower) rather than just the bounding rectangle.
		 * 
		 * @return Returns true if the mouse over the sprite.
		 */
		public static function mouseOverSprite(sprite:Sprite, 
											   shapeFlag:Boolean = true):Boolean 
		{
			return (sprite && sprite.stage && sprite.hitTestPoint(sprite.stage.mouseX, sprite.stage.mouseY, shapeFlag));
		}
        
        
        /**
         * Returns true if value with range of other two values.
         * 
         * @param value Value being checked if within range of other tqo parameters.
         * @param valueA First value defining range limit.
         * @param valueB Second value defining range limit.
         */
        private static function withinRange(value:Number, 
                                            valueA:Number, 
                                            valueB:Number):Boolean 
        {   
            var low:Number;
            var high:Number;
            if (valueA < valueB) 
            {
                low = valueA;
                high = valueB;
            } 
            else 
            {
                low = valueB;
                high = valueA;
            }
            
            if (value < low) 
            {
                return false;
            } 
            else if (value > high)
            {
                return false;
            } 
            else 
            {
                return true;
            }
        }
        
        
        /**
         * Recursively stop any MovieClip children attached to a DisplayObjectContainer
         * 
         * @param container DisplayObjectContainer being stopped
         */
        public static function recursiveStop(container:DisplayObjectContainer):void 
        {
            for (var i:uint = 0; i < container.numChildren; ++i) 
            {
                var child:DisplayObject = container.getChildAt(i);
                if (child is MovieClip) 
                {
                    (child as MovieClip).stop();
                }
                
                if (child is DisplayObjectContainer) 
                {
                    recursiveStop(child as DisplayObjectContainer);
                }
            }
        }
        
        
        /**
         * Make a deep copy of an object.
         * Works particularly well with indexed and associative arrays,
         *  but doesn't work with DisplayObjects. With DisplayObjects 
         *  try var objectClass:Class = Object(source).constructor;
         *      var object:* = new objectClass();
         * 
         * @param object Object being copied
         */
        public static function clone(object:Object):* 
        {
            var data:ByteArray = new ByteArray();
            data.writeObject(object);
            data.position = 0;
            return data.readObject();
        }
        
        
        /**
         * Convert a 0x000000 colorHex uint into RGB percentiles
         * 
         * @param colorHex Color being converted to RGB
         * @return Returns an object with red green blue percentage values
         */
        public static function colorHexToRGB(colorHex:uint):Object 
        {
            var r:Number = ((colorHex & 0xFF0000) >> 16)/255;
            var g:Number = ((colorHex & 0xFF00) >> 8)/255;
            var b:Number = (colorHex & 0xFF)/255;
            
            return { r:r, g:g, b:b };
        }
        
        
        /**
         * Convert RGB percentiles to a 0x000000 colorHex
         * 
         * @param r Red percentile
         * @param g Green percentile
         * @param b Blue percentile
         * @return Resultant colorHex
         */
        public static function RGBtoColorHex(r:Number, g:Number, b:Number):uint 
        {
            var red:uint   = Math.round(r*255);
            var green:uint = Math.round(g*255);
            var blue:uint  = Math.round(b*255);
            return (red << 16) | (green << 8) | blue;
        }
        
        
        /**
         * Convert a 0x000000 colorHex value to a Flash ColorTransform
         */
        public static function makeTransformation(colorHex:uint):ColorTransform
        {
            var rgb:Object = colorHexToRGB(colorHex);
            return new ColorTransform(rgb["r"], rgb["g"], rgb["b"]);
        }
        
        
        /**
         * Convert a Flash ColorTransform to a 0x000000 colorHex value
         */
        public static function makeColorHex(colorTransform:ColorTransform):uint 
        {
            return RGBtoColorHex(colorTransform.redMultiplier, colorTransform.greenMultiplier, colorTransform.blueMultiplier);
        }
        
        
        /**
         * Tint a DisplayObject with a color
         */
        public static function tint(displayObject:DisplayObject, color:uint):void 
        {
            var colorTransform:ColorTransform = makeTransformation(color);
            var transform:Transform = new Transform(displayObject);
            transform.colorTransform = colorTransform;
        }
        
        
        /**
         * Tint a DisplayObject using RGB values
         */
        public static function tintRGB(displayObject:DisplayObject, r:Number, g:Number, b:Number):void 
        {
            var colorTransform:ColorTransform = new ColorTransform();
            colorTransform.redOffset = r;
            colorTransform.greenOffset = g;
            colorTransform.blueOffset = b;
            var transform:Transform = new Transform(displayObject);
            transform.colorTransform = colorTransform;
        }
        
        
        /**
         * Highlight a DisplayObject with a brightness
         * 
         * @param brightness Should be a value from 0 to 255
         */
        public static function highlight(displayObject:DisplayObject, brightness:Number):void 
        {
            var colorTransform:ColorTransform = new ColorTransform();
            colorTransform.redOffset = brightness;
            colorTransform.greenOffset = brightness;
            colorTransform.blueOffset = brightness;
            var transform:Transform = new Transform(displayObject);
            transform.colorTransform = colorTransform;
        }
        
        
        /**
         * Trace all of a DisplayObject's parents
         * 
         * @param displayObject DisplayObject being traced.
         */
        public static function traceParents(displayObject:DisplayObject, string:String = ""):void
        {
            if (displayObject.parent) 
            {
                if (string == "") 
                {
                    string = displayObject.toString();
                } 
                else 
                {
                    string = displayObject + " <-- " + string;
                }
                traceParents(displayObject.parent, string);
            } 
            else 
            {
                trace("[end] <-- " + string);
            }
        }
        
        /**
         * Check if this is a local flash run.
         *  another one worth checking is Capabilities.playerType == "StandAlone";
         *
         * @return Returns a boolean whether the application is running locally or online.
         */
        public static function isLocal(stage:Stage):Boolean
        {
            return stage.loaderInfo.url.indexOf("file:") != -1 ? true : false;
        }
        
        /**
         * Given an object return the class definition
         *
         * @return Returns the class defnition from an object
         */
        public static function getClass(obj:Object):Class 
        {
            return Class(getDefinitionByName(getQualifiedClassName(obj)));
        }
        
        /**
         * Converts https status id collected from HTTPStatusEvent to a descriptive string.
         * httpStatus can give us additional information on what happened during a load.
         * see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
         * 
         * param@ status    The status identifier from a HTTPStatusEvent
         * 
         * return@ Returns the http status as a descriptive string.
         */
        public static function descriptiveHttpStatus(status:int):String
        {
            switch (status) {
                case 100:           return "(100) Continue";
                case 101:           return "(101) Switching Protocols";
                case 200:           return "(200) OK";
                case 201:           return "(201) Created";
                case 202:           return "(202) Accepted";
                case 203:           return "(203) Non-Authoritative Information";
                case 204:           return "(204) No Content";
                case 205:           return "(205) Reset Content";
                case 206:           return "(206) Partial Content";
                case 300:           return "(300) Multiple Choices";
                case 301:           return "(301) Moved Permanently";
                case 302:           return "(302) Found";
                case 303:           return "(303) See Other";
                case 304:           return "(304) Not Modified";
                case 305:           return "(305) Use Proxy";
                case 306:           return "(306) (Unused)";
                case 307:           return "(307) Temporary Redirect";
                case 400:           return "(400) Bad Request";
                case 401:           return "(401) Unauthorized";
                case 402:           return "(402) Payment Required";
                case 403:           return "(403) Forbidden";
                case 404:           return "(404) Not Found";
                case 405:           return "(405) Method Not Allowed";
                case 406:           return "(406) Not Acceptable";
                case 407:           return "(407) Proxy Authentication Required";
                case 408:           return "(408) Request Timeout";
                case 409:           return "(409) Conflict";
                case 410:           return "(410) Gone";
                case 411:           return "(411) Length Required";
                case 412:           return "(412) Precondition Failed";
                case 413:           return "(413) Request Entity Too Large";
                case 414:           return "(414) Request-URI Too Long";
                case 415:           return "(415) Unsupported Media Type";
                case 416:           return "(416) Requested Range Not Satisfiable";
                case 417:           return "(417) Expectation Failed";
                case 500:           return "(500) Internal Server Error";
                case 501:           return "(501) Not Implemented";
                case 502:           return "(502) Bad Gateway";
                case 503:           return "(503) Service Unavailable";
                case 504:           return "(504) Gateway Timeout";
                default:            return "Could not recognize http status of " + status;
            }
        }
        
        /**
         * Convenience method that uses a closure to bind a function callback 
         * with arguments to be called whenever a particular event is dispatched
         * from an EventDispatcher object.
         * 
         * Flash has an annoying bug with closures in that values passed into a 
         * closure are copied by reference, not value. If you try to use multiple 
         * closures in a method passing different references for a variable it won't 
         * work properly as only the last reference will be used for all closures. 
         * This static method sidesteps the problem as references are copied when 
         * passed as parameters into the method.
         * 
         * Note: The listener added is a hard listener and will only dispose
         * when the dispatcher being listened to is cleared.
         * 
         * @param dispatcher EventDispatcher being listened to.
         * @param type Event type being listened for.
         * @param funct Function callback called when event dispatched.
         * @param args Arguments sent to function callback.
         */
        public static function bindOnEvent(dispatcher:IEventDispatcher,
                                           type:String,
                                           funct:Function,
                                           args:Array = null):void
        {
            var listener:Function = function (event:Event):void 
            {
                funct.apply(null, args);
            }
            dispatcher.addEventListener(type, listener);
        }
        
        
        /**
         * Convenience method that, just once, uses a closure to call a function  
         * callback with arguments when a particular event is dispatched from an 
         * EventDispatcher object.
         * 
         * Flash has an annoying bug with closures in that values passed into a 
         * closure are copied by reference, not value. If you try to use multiple 
         * closures in a method passing different references for a variable it won't 
         * work properly as only the last reference will be used for all closures. 
         * This static method sidesteps the problem as references are copied when 
         * passed as parameters into the method.
         * 
         * @param dispatcher EventDispatcher being listened to.
         * @param type Event type being listened for.
         * @param funct Function callback called when event dispatched.
         * @param args Arguments sent to function callback.
         */
        public static function onceOnEvent(dispatcher:IEventDispatcher,
                                           type:String,
                                           funct:Function,
                                           args:Array = null):void
        {
            var listener:Function = function (event:Event):void 
            {
                dispatcher.removeEventListener(type, listener);
                funct.apply(null, args);
            }
            dispatcher.addEventListener(type, listener);
        }
        
        /**
         * Plots the position of a Sprite on stage.
         * 
         * @param sprite Sprite being plotted.
         * @param color Color to draw plot.
         * @param dotSize Size of dot to use when drawing plot.
         * @return Returns the plot Shape that was added to the stage.
         */
        public static function plotPosition(displayObject:DisplayObject, 
                                            color:uint = 0xFF0000,
                                            dotSize:Number = 4):Shape 
        {
            var stage:Stage = displayObject.stage;
            if (! stage) 
            {
                // displayObject has to be on the stage to be able to plot to it.
                return null;
            }
            
            var location:Point = globalLocation(displayObject);
            var box:Shape = makeBoxShape(dotSize, dotSize, color);
            box.x = location.x - dotSize/2;
            box.y = location.y - dotSize/2;
            stage.addChild(box);
            
            return box;
        }
        
        
        /**
         * Plots the rect of a Sprite on stage.
         * 
         * @param displayObject DisplayObject whose bounds are being drawn.
         * @param color Color to draw bounding box.
         * @param lineThickness Line thinkness of bounding box.
         * @param paddingDX Size increase added to left and right.
         * @param paddingDY Size increase added to top and bottom.
         * 
         * @return Returns the bounds Shape that was added to the stage.
         */
        public static function plotBounds(displayObject:DisplayObject,
                                          color:uint = 0xFF0000,
                                          lineThickness:Number = 1, 
                                          paddingDX:Number = 0, 
                                          paddingDY:Number = 0):Shape 
        {
            var stage:Stage = displayObject.stage;
            if (! stage) 
            {
                // displayObject has to be on the stage to be able to plot to it.
                return null;
            }
            
            var rectangle:Rectangle = displayObject.getBounds(stage);
            rectangle.inflate(paddingDX, paddingDY);
            var box:Shape = makeBoxShape(rectangle.width, rectangle.height, color, lineThickness);
            var topLeft:Point = stage.localToGlobal(rectangle.topLeft);
            box.x = topLeft.x;
            box.y = topLeft.y;
            stage.addChild(box);
            
            return box;
        }
        
        
        /**
         * Gives the rect of a DisplayObject on stage.
         * 
         * @param displayObject DisplayObject whose bounds are being drawn.
         * 
         * @return Returns the bounds of a DisplayObject on the stage
         */
        public static function bounds(displayObject:DisplayObject):Rectangle
        {
            if (! displayObject) 
            {
                return null;
            }
            
            var stage:Stage = displayObject.stage;
            if (! stage) 
            {
                // displayObject has to be on the stage to be able to get its bounds
                return null;
            }
            
            return displayObject.getBounds(stage);
        }
        
        
        /**
         * Creates a rectangle "box" Shape of specific size and color,
         * where x = 0, y = 0 is the top left of the rectangle.
         *
         * @param width Width of the box in pixels.
         * @param height Height of the box in pixels.
         * @param color Color of box, e.g. 0xFF0000 is red.
         * @param lineThickness Thickness of line to be draw, 0 indicates a solid box.
         * @return Returns the box Shape.
         */
        public static function makeBoxShape(width:Number,
                                            height:Number, 
                                            color:uint = 0xFF0000,
                                            lineThickness:Number = 0):Shape 
        {
            var box:Shape = new Shape();
            
            if (lineThickness) 
            {
                // use a line thickness
                box.graphics.lineStyle(lineThickness, color);
            } 
            else 
            {
                // fill the box
                box.graphics.beginFill(color);
            }
            
            // draw the box
            box.graphics.drawRect(0, 0, width, height);
            
            return box;
        }
        
        
        /**
         * Creates a circle Shape of specific radius and color,
         * where x = 0, y = 0 is the center of the circle.
         *
         * @param radius Radius of the circle in pixels.
         * @param color Color of box, e.g. 0xFF0000 is red.
         * @param lineThickness Thickness of line to be draw, 0 indicates a solid circle.
         * @return Returns the circle Shape.
         */
        public static function makeCircleShape(radius:Number,
                                               color:uint = 0xFF0000,
                                               lineThickness:Number = 0):Shape 
        {
            var circle:Shape = new Shape();
            
            if (lineThickness) 
            {
                // use a line thickness
                circle.graphics.lineStyle(lineThickness, color);
            } 
            else 
            {
                
                // fill the circle
                circle.graphics.beginFill(color);
            }
            
            // draw the box
            circle.graphics.drawCircle(0, 0, radius);
            
            return circle;
        }
        
        
        /**
         * Creates a polygon Shape of specific size and color,
         * where x = 0, y = 0 is the center of the polygon.
         *
         * @param numberOfSides Number of sides of the polygon (3 = triangle, 4 = diamond, 6 = hexagon, 8 = octagon, etc).
         * @param length Length of each side of the polygon
         * @param rotation Rotation of polygon in degrees.
         * @param color Color of polygon, e.g. 0xFF0000 is red.
         * @param lineThickness Thickness of polygon line to be draw, 0 indicates a solid polygon.
         * @return Returns the polygon Shape.
         */
        public static function makePolygonShape(numberOfSides:uint,
                                                length:Number,
                                                rotation:Number = 0,
                                                color:uint = 0xFF0000,
                                                lineThickness:Number = 0):Shape 
        {
            var polygon:Shape = new Shape();
            var graphics:Graphics = polygon.graphics;
            
            // require 3 or more sides
            if (numberOfSides < 3)
            {
                throw new Error("Polygon must have at least 3 sides.");
            }
            
            if (lineThickness) 
            {
                // use a line thickness
                graphics.lineStyle(lineThickness, color);
            } 
            else
            {
                // fill the polygon
                graphics.beginFill(color);
            }
            
            var radians:Number = degreesToRadians(rotation);
            var angle:Number = (2*Math.PI) / numberOfSides;
            var radius:Number = (length/2) / Math.sin(angle/2);
            var x:Number = Math.cos(radians) * radius;
            var y:Number = Math.sin(radians) * radius;
            
            graphics.moveTo(x, y);
            
            for (var i:int = 1; i <= numberOfSides; i++)
            {
                x = (Math.cos((angle * i) + radians) * radius);
                y = (Math.sin((angle * i) + radians) * radius);
                graphics.lineTo(x, y);
            }
            
            return polygon;
        }
        
        
        /**
         * Creates a "pie" Shape of specific radius and color,
         * where x = 0, y = 0 is the tip of the pie.
         *
         * @param radius Radius of the pie in pixels.
         * @param arc Arc length in degrees.
         * @param color Color of pie, e.g. 0xFF0000 is red.
         * @param lineThickness Thickness of pie line to be draw, 0 indicates a solid pie.
         * @return Returns the pie Shape.
         */
        public static function makePieShape(radius:Number,
                                            arc:Number,
                                            color:uint = 0xFF0000,
                                            lineThickness:Number = 0):Shape 
        {
            var pie:Shape = new Shape();
            var graphics:Graphics = pie.graphics;
            
            if (lineThickness) 
            {
                // use a line thickness
                graphics.lineStyle(lineThickness, color);
            } 
            else
            {
                // fill the polygon
                graphics.beginFill(color);
            }
            
            var arcRadians:Number = degreesToRadians(arc);
            
            var angleDelta:Number = arcRadians/8;
            var dist:Number = radius/Math.cos(angleDelta/2);
            var angle:Number = 0;
            var ctrlX:Number;
            var ctrlY:Number;
            var anchorX:Number;
            var anchorY:Number;
            var startX:Number = Math.cos(0) * radius;
            var startY:Number = Math.sin(0) * radius;
            
            graphics.lineTo(startX, startY);
            for (var i:int = 0; i < 8; i++) 
            {
                angle += angleDelta;
                
                ctrlX = Math.cos(angle-(angleDelta/2))*(dist);
                ctrlY = Math.sin(angle-(angleDelta/2))*(dist);
                
                anchorX = Math.cos(angle) * radius;
                anchorY = Math.sin(angle) * radius;
                
                graphics.curveTo(ctrlX, ctrlY, anchorX, anchorY);
            } 
            graphics.lineTo(0, 0);
            
            return pie;
        }
        
        
        /**
         * Convert radians to degrees.
         * degrees = radians*180/PI
         */
        public static function radiansToDegrees(rad:Number):Number 
        {
            // precalculate 180/PI = 57.295779513082289
            return rad*57.295779513082289;
        }
        
        
        /**
         * Convert degrees to radians.
         * radians = degrees*PI/180
         */
        public static function degreesToRadians(deg:Number):Number 
        {
            // precalculate PI/180 = 0.01745329251994
            return deg*0.01745329251994;
        }
        
        
        /**
         * Return global location of a DisplayObject as a Point
         * 
         * @param displayObject DisplayObject whose global position is being sought
         * @return Global location of displayObject as a point
         */
        public static function globalLocation(displayObject:DisplayObject):Point 
        {
            var point:Point = new Point(displayObject.x, displayObject.y);
            if (displayObject.parent) 
            {
                point = displayObject.parent.localToGlobal(point);
            }
            
            return point;
        }
        
        /**
         * Returns true if all values in a String are numbers
         */
        public static function isNumeric(string:String):Boolean
        {
            for (var i:uint = 0; i < string.length; ++i) 
            {
                var char:String = string.charAt(i);
                switch (char) 
                {
                    case '0':
                    case '1': 
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                    case '.':
                    case ',':
                    case ' ':
                        // ok, is number
                        break;
                    
                    default:
                        return false;
                }
            }
            
            return true;
        }
        
        public static function trimWhitespace(string:String):String 
        {
            if (stringHasValue(string)) 
            {
                return string.replace(/^\s+|\s+$/g, "");
            } 
            else 
            {
                return "";
            }
        }
        
        public static function trimSpaces(string:String):String 
        {
            while(beginsWith(string, ' ')) 
            {
                string = string.substring(1);
            }
            
            while(endsWith(string, ' ')) 
            {
                string = string.substring(0, string.length - 1);
            }
            
            return string;
        }
        
        /**
         * Converts a number to a string and adds zeros in front according
         * to its maximum value.  Useful for filenames where zeros have
         * been added to the beginning of the sequence.
         * 
         * @param value Value being converted to a String.
         * @param maximumValue Maximum value value can be.
         * @return String representation of value.
         */
        public static function addLeadingZeros(value:uint, maximumValue:uint):String 
        {
            var maximumAsString:String = ("" + maximumValue);
            var valueAsString:String = ("" + value);
            
            // add '0''s if any
            for (var i:uint = valueAsString.length; i < maximumAsString.length; ++i) 
            {
                valueAsString = "0" + valueAsString;
            }
            
            return valueAsString;
        }
        
        
        
        /**
         * Returns a String containing all of the publically accessible variables 
         * and their values for the passed in class.
         * 
         * @param  classObject Object
         * @return String
         */ 
        public static function classVariablesDescription(classObject:Object):String
        {
            
            var descriptionString:String = "";
            
            var description:XMLList = describeType(classObject)..variable;
            
            for (var i:int; i < description.length(); i++) 
            {
                // using the += operator on descriptionString sometimes doesn't work.  Don't know the reason why.
                descriptionString = descriptionString.concat("\n" + description[i].@name + ':' + classObject[description[i].@name] + " ");
            }
            
            return descriptionString;
        }
        
        
        /**
         * Make the first letter in a string uppercase, ie --> "color" --> "Color" 
         * 
         * @param  string  String
         * @return String 
         */
        public static function makeFirstLetterUpperCase(string:String):String
        {
            var char:String = string.charAt(0).toUpperCase();
            return (string.length > 1) ? char + string.substr(1, string.length - 1) : char;
        }
        
        /**
         * Specifies whether the specified string is either non-null, or contains
         * characters (i.e. length is greater that 0)
         * 
         * @param string The string which is being checked for a value
         * @return Returns true if the string has a value
         */
        public static function stringHasValue(string:String):Boolean
        {
            return (string != null && string.length > 0);           
        }
        
        /**
         * Replaces all instances of the replace string in the input string
         * with the replaceWith string.
         * 
         * @param input Original string
         * @param replace The string that will be replaced
         * @param replaceWith The string that will replace instances of replace
         * @returns A new String with the replace string replaced with replaceWith
         */
        public static function stringReplace(input:String, 
                                             replace:String, 
                                             replaceWith:String):String
        {
            return input.split(replace).join(replaceWith);
        }
        
        /**
         * Determines whether the specified string begins with the spcified prefix.
         * 
         * @param input The string that the prefix will be checked against.
         * @param prefix The prefix that will be tested against the string.
         * @returns True if the string starts with the prefix, false if it does not.
         */ 
        public static function beginsWith(input:String, prefix:String):Boolean
        {   
            if (! stringHasValue(input) || ! stringHasValue(prefix)) 
            {
                return false;
            }
            return (prefix == input.substring(0, prefix.length));
        }   
        
        
        /**
         * Determines whether the specified string ends with the spcified suffix.
         * 
         * @param input The string that the suffic will be checked against.
         * @param prefix The suffic that will be tested against the string.
         * @returns True if the string ends with the suffix, false if it does not.
         */ 
        public static function endsWith(input:String, suffix:String):Boolean
        {
            if (! stringHasValue(input) || ! stringHasValue(suffix)) 
            {
                return false;
            }
            return (suffix == input.substring(input.length - suffix.length));
        }
        
        
        public static function enclosedBy(input:String, prefix:String, suffix:String):String
        {
            input = trimSpaces(input);
            if (beginsWith(input, prefix) &&
                endsWith(input, suffix)) 
            {
                return input.substring(prefix.length, input.length - suffix.length);
            }
            
            return null;
        }
        
        /**
         * Looks through a String for Array brackets and parses as an array
         */
        public static function findArray(input:String, 
                                         prefix:String = '[', 
                                         suffix:String = ']',
                                         delimiter:String = ","):Array
        {
            input = trimSpaces(input);
            var startIndex:int = input.indexOf(prefix);
            var endIndex:int = input.lastIndexOf(suffix);
            if (startIndex == -1 || endIndex == -1) 
            {
                // can't find prefix or suffix identifiers
                return null;
            }
            
            var contents:String = input.substring(startIndex + 1, endIndex);
            if (! stringHasValue(contents)) 
            {
                // no content
                return [];
            }
            
            var array:Array = contents.split(delimiter);
            
            // trim whitespace
            for (var i:uint = 0; i < array.length; ++i) 
            {
                var item:String = array[i];
                item = trimSpaces(item);
                array[i] = item;
            }
            
            return array;
        }
        
        
        /**
         * Given a URL return the path but exclude any file name
         * 
         * @param url URL String with a path
         * @return Returns the path in the url
         */
        public static function urlPath(url:String):String
        {
            stripCGI(url);
            
            var index:int = url.lastIndexOf('/');
            var path:String = "";
            
            if (index == -1 && 
                url.lastIndexOf('.') == -1) 
            {
                // ensure path ends in a '/'
                if (stringHasValue(url)) 
                {
                    var lastIndex:Number = url.length - 1;
                    var lastCharacter:String = path.charAt(lastIndex);
                    path = url + "/";
                } 
                else 
                {
                    path = "";
                }
            } 
            else 
            {
                index += 1;
                path = url.substring(0, index);
            }
            
            return path;
        }
        
        /**
         * Given a URL return the file name
         * 
         * @param url URL String with file name
         * @return Returns the file name in the url
         */
        public static function urlFileName(url:String):String
        {
            stripCGI(url);
            var index:uint = url.lastIndexOf('/') + 1;
            var fileName:String = url.substring(index);
            return fileName;
        }
        
        public static function fileTypeSuffix(url:String):String
        {
            var suffix:String = stripCGI(url);
            suffix = suffix.slice(suffix.lastIndexOf("."));
            suffix = suffix.toLowerCase();
            return suffix;
        }
        
        public static function truncate(string:String, 
                                        maxChars:uint = 256):String
        {
            if (string.length > maxChars) 
            {
                string = string.slice(0, maxChars - 3);
                string += "...";
            }
            return string;
        }
        
        /**
         * Strips any CGI "?" variables from a url String.
         *
         * @param url URL String being stripped
         * @return Returns the cleaned URL String
         */
        public static function stripCGI(url:String):String
        {
            if (! url) 
            {
                return null;
            }
            
            var index:int = url.indexOf('?');
            if (index > -1) 
            {
                // strip out any "?" CGI data
                url = url.slice(0, index);
            }
            return url;
        }
        
        public static function getDisplayPath(displayObject:DisplayObject):String
        {
            var string:String = "";
            var loaderInfo:LoaderInfo;
            while (displayObject.parent) 
            {
                if (string != "") 
                {
                    string = string + " < ";
                }
                string += displayObject.name + " " + displayObject;
                if (! loaderInfo && displayObject.loaderInfo) 
                {
                    // save the loaderInfo asap if available
                    loaderInfo = displayObject.loaderInfo;
                }
                displayObject = displayObject.parent;
            }
            
            string += ".";
            
            if (loaderInfo) 
            {
                string += "\r\rurl:" + loaderInfo.url + "\r";
            }
            
            return string;
        }
        
        
        /**
         * Prepare a sound by playing it a zero volume.
         * This ensures the sound is played quickly when played, without
         * the annoying pause before a sound is played for the very first
         * time, typical of the Adobe Sound class.
         *
         * @param sound Sound being prepared.
         */
        public static function prepareSound(sound:Sound):void
        {
            sound.play(0, 0, new SoundTransform(0));
        }
        
        
		
		// constants to remember units of millions and billions
		public static const TENS:int 					= 10;			// ten (10^1)
		public static const HUNDREDS:int 				= 100;			// hundred (10^2)
		public static const THOUSANDS:int 				= 1000;			// ten hundred (10^3) is a thousand
		public static const TENS_OF_THOUSANDS:int 		= 10000;		// ten thousand (10^4)
		public static const HUNDREDS_OF_THOUSANDS:int 	= 100000;		// hundred thousand (10^5)
		public static const MILLIONS:int 				= 1000000;		// thousand thousand (10^6) is a million
		public static const TENS_OF_MILLIONS:int 		= 10000000;		// ten million (10^7)
		public static const HUNDREDS_OF_MILLIONS:int 	= 100000000;	// hundred million (10^8)
		public static const BILLIONS:int 				= 1000000000;	// thousand million (10^9) is a billion
		
		/**
		 * Abbreviate number to units of billions, millions, or thousands,
		 * as appropriate, trying to keep length of number under four digits.
		 *
		 * Numbers are returned as a String, are rounded to one decimal place,
		 * and a 'B' (billions), 'M' (millions), or 'K' (thousands) suffix
		 * added to indicate units rounded to.
		 *
		 * @param value Value being abbreviated.
		 * @param fAlwaysAbbrieviateThousands By default only tens of thousands (10,000 = 10K) are abbreviated, setting this to true will always abbreviate thousands (1,000 = 1K).
		 * @return Rounded value formatted as a string.
		 */
		public static function abbreviateNumber(value:Number,
												fAlwaysAbbrieviateThousands:Boolean = false):String
		{
			var absValue:Number = Math.abs(value);
			if (absValue >= BILLIONS)
			{
				return formatToOneDecimalPlace(value, BILLIONS, "B");
			}
			else if (absValue >= MILLIONS)
			{
				return formatToOneDecimalPlace(value, MILLIONS, "M");
			}
			else if (absValue >= TENS_OF_THOUSANDS || (fAlwaysAbbrieviateThousands && absValue >= THOUSANDS))
			{
				return formatToOneDecimalPlace(value, THOUSANDS, "K");
			}
			else
			{
				return formatInteger(value);
			}
		}
		
		public static var THOUSANDS_SEPARATOR:String = ",";
		public static function formatInteger(value:Number, 
											 thousandsSeparator:String = null):String
		{
			if (value == 0 || isNaN(value)) 
			{
				return "0";
			}
			
			if (thousandsSeparator == null)
			{
				thousandsSeparator = THOUSANDS_SEPARATOR;
			}
			
			var string:String;
			var count:int = 0;
			var isNegative:Boolean = value < 0;
			const zero:int = String("0").charCodeAt(0);
			value = isNegative ? -value : value;
			
			string = "";
			for (; value != 0; value = Math.floor(value/10))
			{
				if (++count == 3)
				{
					string = thousandsSeparator + string;
					count = 1;
				}
				var digit:String = String.fromCharCode(zero + int(value % 10));
				string = digit + string;
			}
			
			return isNegative ? "-" + string : string;
		}
		
		/* perhaps this instead of formatInteger() ?
		// TODO: Use AS3 NumberFormatter which takes into account localization
		public static function AddCommasToNumber(number:String):String
		{
			var numString:String = number;
			var result:String = '';
			
			while (numString.length > 3)
			{
				var chunk:String = numString.substr(-3);
				numString = numString.substr(0, numString.length - 3);
				result = ',' + chunk + result
			}
			
			if (numString.length > 0)
			{
				result = numString + result
			}
			
			return result
		}
		*/
		
		/**
		 * Format a value by a divisor, always rounding down
		 * to one decimal place and add an optional suffix.
		 *
		 * @param value Value being formatted.
		 * @param divisor Divisor by which to format down to.
		 * @param suffix Suffix to add to formatted value.
		 * @return Returns value as a formatted string.
		 */
		public static function formatToOneDecimalPlace(value:Number,
													   divisor:int,
													   suffix:String = ""):String
		{
			// find to one decimal place.
			value = Math.floor(value/(divisor/TENS));
			var remainder:int = Math.abs(value%TENS);
			
			// reduce down to divisor by dividing by a furthur ten.
			value = Math.floor(value/TENS);
			
			// convert whole value to a String
			var string:String = formatInteger(value);
			
			// append remainder, if any
			if (remainder)
			{
				string += "." + remainder;
			}
			
			// append suffix
			string += suffix;
			
			return string;
		}

		public static function convertMillisecondsToTimeRemaining(milliseconds:Number):Object
		{
			// ignore negative time remaining
			if (milliseconds < 0)
			{
				milliseconds = 0;
			}
			
			// convert to seconds remaining
			var seconds:Number = Math.floor(milliseconds/1000);
			
			if (seconds <= 60)
			{
				return {dDuration:seconds, stDurationUnits:(seconds == 1) ? "second" : "seconds"};
			}
			
			// convert to minutes remaining
			var minutes:Number = Math.floor(seconds/60);
			if (minutes <= 60)
			{
				return {dDuration:minutes, stDurationUnits:(minutes == 1) ? "minute" : "minutes"};
			}
			
			// convert to hours remaining
			var hours:Number = Math.floor(minutes/60);
			if (hours <= 24)
			{
				return {dDuration:hours, stDurationUnits:(hours == 1) ? "hour" : "hours"};
			}
			
			// convert to days remaining
			var days:Number = Math.floor(hours/24);
			if (days <= 30)
			{
				return {dDuration:days, stDurationUnits:(days == 1) ? "day" : "days"};
			}
			
			// convert to (approx.) months remaining
			var months:Number = Math.floor(days/30);
			return {dDuration:months, stDurationUnits:(months == 1) ? "month" : "months"};
		}
		
		
		
        /**
         * Create a Function that, when called, instantiates a class.
         * This is needed as ActionScript will not allow you to use apply()
         * on a constructor, it only works on a function.
         * 
         * @param classDefinition Class to instantiate
         * @return A function that, when called, instantiates a class with the
         *  arguments passed to said Function or null if the given class is null.
         */
        public static function makeConstructorFunction(classDefinition:Class):Function
        {
            if (classDefinition == null)
            {
                return null;
            }
            
            /**
             * The function to call to instantiate the class
             * @param args Arguments to pass to the constructor. There may be up to 20 arguments.
             * @return The instantiated instance of the class or null if an instance
             *          couldn't be instantiated. This happens if the given class or
             *          arguments are null, there are more than 20 arguments, or the
             *          constructor of the class throws an exception.
             */
            return function(...args:Array): Object
            {
                switch (args.length)
                {
                    case 0:
                        return new classDefinition();
                        break;
                    case 1:
                        return new classDefinition(args[0]);
                        break;
                    case 2:
                        return new classDefinition(args[0], args[1]);
                        break;
                    case 3:
                        return new classDefinition(args[0], args[1], args[2]);
                        break;
                    case 4:
                        return new classDefinition(args[0], args[1], args[2], args[3]);
                        break;
                    case 5:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4]);
                        break;
                    case 6:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5]);
                        break;
                    case 7:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
                        break;
                    case 8:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
                        break;
                    case 9:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
                        break;
                    case 10:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);
                        break;
                    case 11:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10]);
                        break;
                    case 12:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11]);
                        break;
                    case 13:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12]);
                        break;
                    case 14:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13]);
                        break;
                    case 15:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14]);
                        break;
                    case 16:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15]);
                        break;
                    case 17:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16]);
                        break;
                    case 18:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17]);
                        break;
                    case 19:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18]);
                        break;
                    case 20:
                        return new classDefinition(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19]);
                        break;
                    default:
                        break;
                }
                throw new Error(args.length + " exceeds the 20 argument limit.");
            };
        }
    }
}